/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package abricots.game.state;

import abricots.MyLog;
import abricots.net.common.MessageDispatcher;
import abricots.entity.BallEntity;
import abricots.entity.Entity;
import abricots.entity.PlayerEntity;
import abricots.entity.RootEntity;
import abricots.command.PlayerCommand;
import abricots.entity.state.EntityState;
import abricots.game.EntityFactory;
import abricots.game.GameEnv;
import abricots.game.GameUtils;
import abricots.game.TimeManager;
import abricots.net.GameClient;
import abricots.net.common.GameMessage;
import abricots.net.common.GameMessage.EntityMessage;
import abricots.net.common.NetUtils.GameClientListener;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.minlog.Log;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;

/**
 *
 * @author charly
 */
public class TestState extends BasicGameState implements GameClientListener {
   private final MyLog log = new MyLog(getClass().getSimpleName(), Log.LEVEL_DEBUG);
   private GameEnv env;
   private StateBasedGame game;
   private GameClient client;
   private BallEntity entity;
   private PlayerEntity player;
   private final MessageDispatcher dispatcher;
   private final Set<GameMessage.WorldDiffMessage> diffMessages;

   public TestState(GameEnv env, GameClient client) {
      this.env = env;
      this.client = client;
      dispatcher = new TestStateMessageDispatcher(this, env);
      diffMessages = Collections.synchronizedSet(new HashSet<GameMessage.WorldDiffMessage>());
   }

   @Override
   public int getID() {
      return GameUtils.STATE_ID_TEST;
   }

   @Override
   public void init(GameContainer container, StateBasedGame game) throws SlickException {
   }

   @Override
   public void render(GameContainer container, StateBasedGame game, Graphics g) throws SlickException {
      long drawTime = env.getTimeManager().getLastUpdateTime() - TimeManager.DRAW_LAG;
      log.debug("Drawing frame " + drawTime);
      RootEntity rootEntity = env.getEntitiesManager().getRootEntity();
      rootEntity.draw(g, drawTime);
   }

   @Override
   public void update(GameContainer container, StateBasedGame game, int delta) throws SlickException {
      log.debug("====NEW FRAME");
      prepareUpdate();
      extractDiff();
      simulate();
      buildAndSendCommands();
      cleanUpUpdate();
      log.debug("====END FRAME");
   }

   private void prepareUpdate() {
      env.getTimeManager().newUpdate();
      long drawTime = env.getTimeManager().getLastUpdateTime() - TimeManager.DRAW_LAG;
      env.getTimeManager().addIntermediateTime(drawTime);
      log.trace("GameTimes:" + env.getTimeManager().getTimes().toString());
   }

   private void simulate() {
      env.getEntitiesManager().getRootEntity().simulate();
   }

   private void extractDiff() {
      long lastReceivedDiffToTime = -1;
      synchronized (diffMessages) {
         if (diffMessages.isEmpty()) {
            return;
         }
         for (GameMessage.WorldDiffMessage diffMsg : diffMessages) {
            extractDiff(diffMsg);
            if (lastReceivedDiffToTime < 0 || lastReceivedDiffToTime < diffMsg.toTime) {
               lastReceivedDiffToTime = diffMsg.toTime;
            }
         }
         diffMessages.clear();
      }
      if (lastReceivedDiffToTime >= 0) {
         long lastAckedTime = client.getAckedFramesManager().getLastAckedTime();
         long drawTime = env.getTimeManager().getLastUpdateTime() - TimeManager.DRAW_LAG;
         long lastValid = Math.max(lastReceivedDiffToTime, lastAckedTime);
         lastValid = Math.max(lastValid, drawTime);
         env.getEntitiesManager().cleanUp(-1, lastValid);
         log.debug("Cleared all after " + lastValid);
      }
   }

   private void extractDiff(GameMessage.WorldDiffMessage message) {
      log.trace("Extracting diff " + message.toString());
      for (EntityMessage newEntityMsg : message.newEntities) {
         if (!env.getEntitiesManager().containsEntity(newEntityMsg.entityId)) {
            log.trace(" -adding entity " + newEntityMsg.toString());
            Entity newEntity = EntityFactory.build(newEntityMsg.entityTypeId);
            newEntity.register(env);
            env.getEntitiesManager().validateEntity(newEntity.getId(), newEntityMsg.entityId);
         }
      }
      for (EntityMessage oldEntityMsg : message.removedEntities) {
         if (env.getEntitiesManager().containsEntity(oldEntityMsg.entityId)) {
            env.getEntitiesManager().removeEntity(oldEntityMsg.entityId);
            log.trace(" -removed entity " + oldEntityMsg.toString());
         }
      }
      for (EntityState state : message.modifiedStates) {
         Entity modifiedEntity = env.getEntitiesManager().getEntity(state.getEntityId());
         if (modifiedEntity == null) {
            log.warn("No entity present to extract diff : " + state);
            continue;
         }
         log.trace(" -modified entity " + state.toString());
         modifiedEntity.getStateManager().addState(state);
         modifiedEntity.getStateManager().setLastValid(state.getGameTime());
         env.getWorldStateManager().addState(state.getGameTime(), state.getEntityId());
      }
      log.trace("Last received frame: " + message.toTime);
      //TODO: check fromtime usefulnes
   }

   private void buildAndSendCommands() {
      if (!player.isValidated()) {
         EntityMessage entityMessage = new EntityMessage();
         entityMessage.localEntityId = player.getId();
         entityMessage.entityId = -1;
         entityMessage.entityTypeId = player.getEntityTypeId();
         client.send(entityMessage);
         return;
      }
      long lastTime = client.getAckedFramesManager().getLastAckedTime();
      long toTime = env.getTimeManager().getLastUpdateTime();
      GameMessage.CommandsMessage message = entity.getCommandManager().buildCommandMessage(lastTime, toTime);
      int messageId = client.send(message);
      client.getAckedFramesManager().newSentState(messageId, toTime);
   }

   private void cleanUpUpdate() {
      long lastAckedTime = client.getAckedFramesManager().getLastAckedTime();
      long drawTime = env.getTimeManager().getLastUpdateTime() - TimeManager.DRAW_LAG;
      env.getTimeManager().clearTimesBefore(drawTime);
      final long lastNeededTime = Math.min(lastAckedTime, drawTime);
      log.trace("Last needed time: " + lastNeededTime);
      env.getEntitiesManager().cleanUp(lastNeededTime, -1);
   }

   @Override
   public void keyPressed(int key, char c) {
      if (player != null) {
          player.getController().keyPressed(key);
      }

      if (key == Input.KEY_N) {
         if (player == null || entity != null) {
            return;
         }
         entity = new BallEntity();
         entity.getStateManager().addState(EntityFactory.buildStartState(entity.getEntityTypeId()));
         entity.register(env);
//         entity.getController().bind(PlayerCommand.CMD_UP, Input.KEY_UP);
//         entity.getController().bind(PlayerCommand.CMD_DOWN, Input.KEY_DOWN);
//         entity.getController().bind(PlayerCommand.CMD_LEFT, Input.KEY_LEFT);
//         entity.getController().bind(PlayerCommand.CMD_RIGHT, Input.KEY_RIGHT);
         GameMessage.EntityMessage gameMessage = new GameMessage.EntityMessage();
         gameMessage.entityId = -1;
         gameMessage.localEntityId = entity.getId();
         gameMessage.entityTypeId = entity.getEntityTypeId();
         log.debug("Sending new entity message : " + gameMessage);
         client.send(gameMessage);
      }
   }

   @Override
   public void keyReleased(int key, char c) {
      if (entity != null) {
         player.getController().keyReleased(key);
      }
   }

   public BallEntity getEntity() {
      return entity;
   }

   @Override
   public void messageReceived(Object message, Connection connection) {
      log.trace("Message received: " + message.toString());
      dispatcher.dispatchMessage(message, connection);
   }

   public void askUnknownEntity(int id) {
      if (id == -1) {
         log.error("Asked to ask for unknown entity -1");
      }
      GameMessage.EntityMessage entityMessage = new GameMessage.EntityMessage();
      entityMessage.entityId = id;
      entityMessage.entityTypeId = -1;
      client.send(entityMessage);
   }

   @Override
   public void connectionFailed() {
   }

   @Override
   public void onConnection(Connection connection) {
   }

   @Override
   public void onDisConnection(Connection connection) {
      game.enterState(GameUtils.STATE_ID_CONNECTING);
   }

   @Override
   public void enter(GameContainer container, StateBasedGame game) throws SlickException {
      super.enter(container, game);
      client.addListener(this);
      if (player == null) {
         player = new PlayerEntity(env);
         player.register(env);
      }
   }

   @Override
   public void leave(GameContainer container, StateBasedGame game) throws SlickException {
      super.leave(container, game);
      client.removeListener(this);
   }

   public GameClient getClient() {
      return client;
   }

   public void addDiffMessage(GameMessage.WorldDiffMessage message) {
      synchronized (diffMessages) {
         diffMessages.add(message);
      }
   }
}
